Keep PR review collaboration alive beyond author sessions - #573
Conversation
PR handoffs previously depended on ad-hoc chats and a live author session. Add an idempotent PR Room lifecycle that creates or adopts a Feishu group, prepares local and federated reviewer agents, persists setup intent, and lets the author repair or finish the room safely. Constraint: Feishu bot open_id values are app-scoped, so remote reviewer readiness must be acknowledged by the owning deployment before kickoff. Constraint: Cross-deployment simultaneous open calls do not yet share a distributed uniqueness lock. Rejected: Retry expired or timed-out group creation automatically | could create duplicate review groups after an ambiguous remote success. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve the setup attempt token and finish-pending invariants before adding new PR Room side effects. Tested: pnpm build; tsc --noEmit; 63 targeted unit tests; full unit suite with only 15 baseline environment failures; dual code and architecture review. Not-tested: Live cross-deployment Feishu group creation against two production Botmux deployments.
|
@deepcoldy 已完成实现并请求 Code Owner review。验证包括 我也用本分支实际创建了对应的 Botmux PR Room,双方 agent 已入群、作者 worktree 已绑定。由于你的部署尚未包含本 PR 新增的 |
|
将此 PR 退回 Draft,暂停实现推进与合入。 原因:这个能力涉及 Botmux 的群组、federation、session、权限信任、调度和生命周期边界,应该先完成对既有能力与架构的系统盘点,再决定是否需要新增 |
deepcoldy
left a comment
There was a problem hiding this comment.
Review: PR Review Room 生命周期 + 联邦建群 + 原子 claim 并发
审查方式:钉 PR head f208d017 隔离 checkout(pnpm install --frozen-lockfile + pnpm build),跑 PR 全部新增/改动测试 + 全量 unit/e2e,对当前 master 做 trial-merge 与回归对拍,并对状态机 / 失败回滚 / 联邦端点做了对抗式并发分析(含对纯函数与错误分类逻辑的实机复现)。
✅ 验证通过的部分
- 构建/类型:
pnpm build绿、tsc干净。 - 测试:PR 自带 4 个测试文件共 56 例全过(
pr-room/webhook-lifecycle-store/cli-arg-utils/federation-api)。全量套件里 21 个失败文件中,18 个是 e2e/browser(需 live 飞书/浏览器/CLI),3 个非 e2e(scheduler/schedule-card-model/v3-distillation-runner)在干净 master 上同样失败——本机时间敏感基线,对 master 零回归。 - 共享 store 影响面:
webhook-lifecycle-store是既有webhook-routes.ts的公共依赖。改动用重载保留了 2 参旧签名,旧 caller(opts={})走的blockResolvedReopen/blockIndeterminateRetry/adoptIndeterminate全 falsy,行为与 master 逐字一致;新增的beginWebhookLifecycleSetupRepair/complete/isCurrent/markIndeterminate/resolve的新语义仅pr-room.ts消费,隔离干净。 - 合并:对当前 master 仅
README.md有机械冲突(master 重写了开头段落,PR 的pr-room文档块本身干净保留),src/cli.ts自动合并。package.json的build脚本看似删了generate-runtime-build-id.mjs——实为分支老(229 commits 前),该步是分支点后才加到 master 的,3-way merge 会保留 master 版本,非回归。需 rebase。 - 状态机核心:
creating/active/resolved+setup子状态 +setupRepairIdclaim(30min TTL)设计扎实。双重 kickoff、kickoff-after-resolve、并发 repair 都被「发送前紧邻assertSetupRepairCurrent+busy/setupReviewersReady闸门」正确围栏。联邦delegate-prepare-review端点鉴权在副作用前、reviewer 强制 local + in-chat 校验、teamId 取自鉴权 membership(不可伪造)、幂等 key 与 3 个姊妹端点同构,安全。纯函数parsePullRequestRef边界(10 组输入实测)、argValues、resolveRosterBotRef均正确。
🟠 建议修复(P2,非合并硬阻塞,但都属「本 PR 明确想防的失败模式」)
P2-1 · openPrRoom setup 阶段抛异常会把 repairing claim 悬挂 30 分钟(与 adopt/repair 不对称)
src/cli/pr-room.ts:405-406(workdir 分支)与 :424(prepare 前)的 assertSetupRepairCurrent / bindOncall 在任何 try/catch 之外,且 openPrRoom 无 try/finally 保证 completeWebhookLifecycleSetupRepair(:499)一定跑到。而 bindOncall 对预期失败返回 {ok:false}(被当 degraded 处理),但底层 rmwBotEntry→withFileLock(bots.json) 在锁竞争超时(MAX_WAIT_MS=5000)或 fs I/O 错误时抛异常——oncall-store 头注释明确写了「12 daemon + 1 dashboard 抢 bots.json、并发 auto-bind 突发」,5s 锁超时抛错是现实可达的。异常逃逸到 cmdPrRoom 顶层 catch(:993)只 console.error+exitCode=1,从不释放 claim。
后果(30 分钟窗口内):repair 命中 busy 闸门 → 拒绝执行;open 复用后仍提示「去 repair」→ 死循环;finish 因 waitsForSetup=true 挂 pendingResolved 但 setup 永不完成 → 房间无法关闭。只能等 30min TTL 自愈。adoptPrRoom(:648-696 每段 try + 无条件 complete)和 repairPrRoom(:catch 里必调 complete)都没这问题。
修法:把 open 的 :401-498 setup 区包进 try/catch(或 try/finally),异常路由到 degradedReasons 并保证 completeWebhookLifecycleSetupRepair(error=...) 一定执行,与 repair 的结构对齐。
P2-2 · 网关 5xx 被误判为「确定未建群」→ 重复建群风险
src/cli/pr-room.ts:213-221 + isAmbiguousTransportError(:155-162)。pr-room open 走本地 createLocalTeamGroup,orchestrateFederatedGroup 在此路径永不返回 408/504(那是 no_online_daemon 联邦委派分支才有),所以 isAmbiguousGroupCreateFailure 的 status===408||504 检查在本路径是死代码,歧义判定 100% 落在 isAmbiguousTransportError 对抛出错误的匹配上。
实测(复刻该函数逐一喂错误形状):Lark SDK 是 axios,HTTP ≥400 会 throw e(原始 AxiosError)。网关/反代在请求已转发给 Lark 之后返回的 504/502,形状是 code:'ERR_BAD_RESPONSE' + message "Request failed with status code 504"——isAmbiguousTransportError 判定为 definite → 走 failWebhookLifecycleGroup 删掉 creating 记录 → 下次 open 重新建群 = 重复群。而 socket hang up / ECONNRESET / ETIMEDOUT / AbortError 这些是被正确判 ambiguous 的,never-connected(ECONNREFUSED/ENOTFOUND)判 definite 也对。缺口只在「请求已发出、响应层 5xx」这一类。可达性取决于出网是否经反代,SUSPECTED-medium。
修法:createGroupWithBots 一旦请求发出后抛的任何异常都应视为 ambiguous(建群与否不可知),只有 pre-flight / 从未发出的错误(连接被拒、DNS)才判 definite。或在 createLocalTeamGroup 的 catch 里对 AxiosError 带 response.status>=500(甚至任何 response 存在)显式归 group_create_indeterminate:。
🔵 次要观察(可选,非阻塞)
resolveReviewerMentionOpenIds(:131-144)单遍贪心 + 共享used对同 display-name 的 reviewer 有次序依赖——但实测证实无实际危害:真正在群里、可唯一解析的 bot 永远会进openIds(被 ping 到),次序只影响missing诊断标签在同名项间的归属(cosmetic)。想更干净可两遍解析(先绑 exact larkAppId,再 display-name)。delegate-prepare-review的信任门isTeamGroupChat跨 team 匹配 chatId(忽略 teamId),即一个 team 下记录的绑定会全局授予 bot-to-bot 信任——在既有联邦信任范围内且是端点本意,仅提示。- adopt 路径若 reviewer 的远端部署在 roster 构建与 prepare 之间离开联邦(TOCTOU),该 reviewer 既不进
ready也不进failed,adopt 仍会 @ 它(依赖后续周期同步兜底)——窄且是「欠授信」非「越权」。 adoptPrRoom里begun.action === 'indeterminate'分支是死代码(adopt 传adoptIndeterminate:true只会得到reconcile),可清理。
结论
核心并发设计正确、隔离干净、零回归、测试充分——工程质量高。两个 P2 都命中了「本 PR 明确要防的失败模式」(悬挂 claim / 重复群),建议合并前修掉,尤其 P2-1(多 daemon 抢 bots.json 时现实可达)。另需 rebase 解 README 机械冲突。
审查覆盖:build/tsc/56 新测试/全量套件回归对拍/trial-merge/状态机 & 失败回滚 & 联邦端点对抗式分析 + 纯函数与错误分类实机复现。fork PR 无 CI,以上均为本地隔离验证。
What changed
botmux pr-room open|adopt|repair|finish|listfor a durable PR-scoped Feishu review group.adoptorfinish + --reopenreconciliation.Why
PR review handoffs currently depend on an ad-hoc group and a live author session. If the author session closes or the Owner creates a separate group, agent context and follow-up can be lost. PR Rooms make the collaboration durable and recoverable until merge, close, or explicit abandonment.
Safety and concurrency
finishwaits for a live setup attempt, then resolves atomically.Validation
pnpm buildtsc --noEmitnode:sqlite, fake Codex ESM fixture, Linux process scan/timeouts, hook/MCP sandbox environment)git diff --checkKnown boundary
Cross-deployment simultaneous
opencalls do not yet share a distributed uniqueness lock. The documented contract is to invokeopenfrom the PR author's hub/main deployment.